<!DOCTYPE html>
<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]-->
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  
  <meta name="author" content="Mouse0w0">
  
  <link rel="shortcut icon" href="../img/favicon.ico">
  <title>粒子 - Lwjglbook中文翻译</title>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:400,700|Roboto+Slab:400,700|Inconsolata:400,700" />

  <link rel="stylesheet" href="../css/theme.css" />
  <link rel="stylesheet" href="../css/theme_extra.css" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/github.min.css" />
  
  <script>
    // Current page data
    var mkdocs_page_name = "\u7c92\u5b50";
    var mkdocs_page_input_path = "20-particles.md";
    var mkdocs_page_url = null;
  </script>
  
  <script src="../js/jquery-2.1.1.min.js" defer></script>
  <script src="../js/modernizr-2.8.3.min.js" defer></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
  <script>hljs.initHighlightingOnLoad();</script> 
  
</head>

<body class="wy-body-for-nav" role="document">

  <div class="wy-grid-for-nav">

    
    <nav data-toggle="wy-nav-shift" class="wy-nav-side stickynav">
    <div class="wy-side-scroll">
      <div class="wy-side-nav-search">
        <a href=".." class="icon icon-home"> Lwjglbook中文翻译</a>
        <div role="search">
  <form id ="rtd-search-form" class="wy-form" action="../search.html" method="get">
    <input type="text" name="q" placeholder="Search docs" title="Type search term here" />
  </form>
</div>
      </div>

      <div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../01-first-steps/">事前准备</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../02-the-game-loop/">游戏循环</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../03-a-brief-about-coordinates/">坐标简介</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../04-rendering/">渲染</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../05-more-on-rendering/">渲染补充</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../06-transformations/">变换</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../07-textures/">纹理</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../08-camera/">摄像机</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../09-loading-more-complex-models/">加载更复杂的模型</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../10-let-there-be-light/">要有光</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../11-let-there-be-even-more-light/">要有更多的光</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../12-game-hud/">游戏HUD</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../13-sky-box-and-some-optimizations/">天空盒与一些优化</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../14-height-maps/">高度图</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../15-terrain-collisions/">地形碰撞</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../16-fog/">雾</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../17-normal-mapping/">法线贴图</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../18-shadows/">阴影</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../19-animations/">动画</a>
                    </li>
                </ul>
                <ul class="current">
                    <li class="toctree-l1 current"><a class="reference internal current" href="./">粒子</a>
    <ul class="current">
    <li class="toctree-l2"><a class="reference internal" href="#_1">基础</a>
    </li>
    <li class="toctree-l2"><a class="reference internal" href="#texture-atlas">纹理集（Texture Atlas）</a>
    </li>
    </ul>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../21-instanced-rendering/">实例化渲染</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../22-audio/">音效</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../23-3d-object-picking/">三维物体选取</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../24-hud-revisited/">回顾HUD - NanoVG</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../25-optimizations-frustum-culling/">优化 - 截锥剔除</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../26-cascaded-shadow-maps/">级联阴影映射</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../27-assimp/">Assimp库</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../28-deferred-shading/">延迟着色法</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../a01-opengl-debugging/">附录 A - OpenGL调试</a>
                    </li>
                </ul>
                <ul>
                    <li class="toctree-l1"><a class="reference internal" href="../glossary/">术语表</a>
                    </li>
                </ul>
      </div>
    </div>
    </nav>

    <section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">

      
      <nav class="wy-nav-top" role="navigation" aria-label="top navigation">
        <i data-toggle="wy-nav-top" class="fa fa-bars"></i>
        <a href="..">Lwjglbook中文翻译</a>
      </nav>

      
      <div class="wy-nav-content">
        <div class="rst-content">
          <div role="navigation" aria-label="breadcrumbs navigation">
  <ul class="wy-breadcrumbs">
    <li><a href="..">Docs</a> &raquo;</li>
    
      
    
    <li>粒子</li>
    <li class="wy-breadcrumbs-aside">
      
        <a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/edit/master/docs/20-particles.md"
          class="icon icon-github"> Edit on GitHub</a>
      
    </li>
  </ul>
  
  <hr/>
</div>
          <div role="main">
            <div class="section">
              
                <h1 id="particles">粒子（Particles）</h1>
<h2 id="_1">基础</h2>
<p>在本章中，我们将添加粒子效果到游戏引擎中。有了这种效果，我们就能模拟关系、火、灰尘和云。这是一种简单的效果，将改善对任何游戏的图形方面。</p>
<p>在此之前值得一提的是，有很多方法可以实现不同效果的粒子效果。当前情况下，我们将使用面板粒子（Billboard Particle）。该技术使用移动的纹理四边形来表示一个粒子，它们总是面向观察者，在本例中，就是摄像机。你还可以使用面板技术在游戏项上显示信息面板，比如迷你HUD。</p>
<p>让我们开始定义粒子，粒子可以通过以下属性定义：</p>
<ol>
<li>一个用于表示四边形顶点的网格。</li>
<li>一张纹理。</li>
<li>某一时刻的坐标。</li>
<li>缩放系数。</li>
<li>速度。</li>
<li>移动方向。</li>
<li>生存时间或存活时间。一旦该时间过去，粒子就不再存在。</li>
</ol>
<p>前四项是<code>GameItem</code>类的一部分，但后三项不是。因此，我们要创建一个名为<code>Particle</code>的新类，它继承了<code>GameItem</code>类，其定义如下：</p>
<pre><code class="java">package org.lwjglb.engine.graph.particles;

import org.joml.Vector3f;
import org.lwjglb.engine.graph.Mesh;
import org.lwjglb.engine.items.GameItem;

public class Particle extends GameItem {

    private Vector3f speed;

    /**
     * 粒子存活的时间，以毫秒为单位
     */
    private long ttl;

    public Particle(Mesh mesh, Vector3f speed, long ttl) {
        super(mesh);
        this.speed = new Vector3f(speed);
        this.ttl = ttl;
    }

    public Particle(Particle baseParticle) {
        super(baseParticle.getMesh());
        Vector3f aux = baseParticle.getPosition();
        setPosition(aux.x, aux.y, aux.z);
        aux = baseParticle.getRotation();
        setRotation(aux.x, aux.y, aux.z);
        setScale(baseParticle.getScale());
        this.speed = new Vector3f(baseParticle.speed);
        this.ttl = baseParticle.geTtl();
    }

    public Vector3f getSpeed() {
        return speed;
    }

    public void setSpeed(Vector3f speed) {
        this.speed = speed;
    }

    public long geTtl() {
        return ttl;
    }

    public void setTtl(long ttl) {
        this.ttl = ttl;
    }

    /**
     * 更新粒子的存活时间
     * @param elapsedTime 经过的时间（毫秒）
     * @return 粒子的存活时间
     */
    public long updateTtl(long elapsedTime) {
        this.ttl -= elapsedTime;
        return this.ttl;
    }
}
</code></pre>

<p>从上述代码可以看出，粒子的速度和运动方向可以表示为一个向量。该向量的方向决定了粒子的运动方向和速度。粒子存活时间（TTL）被设定为毫秒计数器，每当更新游戏状态时，它都会减少。该类还有一个复制构造函数，也就是说，一个构造函数接收另一个粒子实例来进行复制。</p>
<p>现在，我们需要创建一个粒子生成器或粒子发射器，即一个动态生成粒子、控制其生命周期并根据特定的模式更新其位置的类。我们可以创建很多实现，它们在粒子的创建方式和位置的更新方式（例如，是否考虑重力）方面各不相同。因此，为了保持游戏引擎的通用性，我们将创建一个所有粒子发射器必须要实现的接口。这个名为<code>IParticleEmitter</code>的接口定义如下：</p>
<pre><code class="java">package org.lwjglb.engine.graph.particles;

import java.util.List;
import org.lwjglb.engine.items.GameItem;

public interface IParticleEmitter {

    void cleanup();

    Particle getBaseParticle();

    List&lt;GameItem&gt; getParticles();
}
</code></pre>

<p><code>IParticleEmitter</code>接口有一个清理资源的方法，名为<code>cleanup</code>，还有一个获取粒子列表的方法，名为<code>getParticles</code>。还有一个名为<code>getBaseParticle</code>的方法，但是这个方法是做什么的呢？一个粒子发射器将动态地产生许多例子。每当一个粒子过期，就会创建新的粒子。该粒子更新周期将使用基础粒子作为模板创建新的势力。这就是基础粒子的用途，这也是为什么<code>Particle</code>类定义了一个复制构造函数。</p>
<p>在游戏引擎的代码中，我们将只引用<code>IParticleEmitter</code>接口，因此基础代码将不依赖于特定的实现。不过，我们可以创建一个实现来模拟不受重力影响的粒子流。这个实现可以用来模拟光线或火焰，名为<code>FlowParticleEmitter</code>。</p>
<p>这个类的行为可以通过以下属性进行调整：</p>
<ul>
<li>一次能存在的最大粒子数量</li>
<li>创建粒子的最短周期。粒子将在最短的时间内一个接一个地创建，以避免粒子爆发性创建。</li>
<li>一组范围，以随机粒子速度和位置。新粒子将使用基础粒子的位置和速度，可以在相应范围内取值，以分散光线。</li>
</ul>
<p>该类的实现如下：</p>
<pre><code class="java">package org.lwjglb.engine.graph.particles;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.joml.Vector3f;
import org.lwjglb.engine.items.GameItem;

public class FlowParticleEmitter implements IParticleEmitter {

    private int maxParticles;

    private boolean active;

    private final List&lt;GameItem&gt; particles;

    private final Particle baseParticle;

    private long creationPeriodMillis;

    private long lastCreationTime;

    private float speedRndRange;

    private float positionRndRange;

    private float scaleRndRange;

    public FlowParticleEmitter(Particle baseParticle, int maxParticles, long creationPeriodMillis) {
        particles = new ArrayList&lt;&gt;();
        this.baseParticle = baseParticle;
        this.maxParticles = maxParticles;
        this.active = false;
        this.lastCreationTime = 0;
        this.creationPeriodMillis = creationPeriodMillis;
    }

    @Override
    public Particle getBaseParticle() {
        return baseParticle;
    }

    public long getCreationPeriodMillis() {
        return creationPeriodMillis;
    }

    public int getMaxParticles() {
        return maxParticles;
    }

    @Override
    public List&lt;GameItem&gt; getParticles() {
        return particles;
    }

    public float getPositionRndRange() {
        return positionRndRange;
    }

    public float getScaleRndRange() {
        return scaleRndRange;
    }

    public float getSpeedRndRange() {
        return speedRndRange;
    }

    public void setCreationPeriodMillis(long creationPeriodMillis) {
        this.creationPeriodMillis = creationPeriodMillis;
    }

    public void setMaxParticles(int maxParticles) {
        this.maxParticles = maxParticles;
    }

    public void setPositionRndRange(float positionRndRange) {
        this.positionRndRange = positionRndRange;
    }

    public void setScaleRndRange(float scaleRndRange) {
        this.scaleRndRange = scaleRndRange;
    }

    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }

    public void setSpeedRndRange(float speedRndRange) {
        this.speedRndRange = speedRndRange;
    }

    public void update(long ellapsedTime) {
        long now = System.currentTimeMillis();
        if (lastCreationTime == 0) {
            lastCreationTime = now;
        }
        Iterator&lt;? extends GameItem&gt; it = particles.iterator();
        while (it.hasNext()) {
            Particle particle = (Particle) it.next();
            if (particle.updateTtl(ellapsedTime) &lt; 0) {
                it.remove();
            } else {
                updatePosition(particle, ellapsedTime);
            }
        }

        int length = this.getParticles().size();
        if (now - lastCreationTime &gt;= this.creationPeriodMillis &amp;&amp; length &lt; maxParticles) {
            createParticle();
            this.lastCreationTime = now;
        }
    }

    private void createParticle() {
        Particle particle = new Particle(this.getBaseParticle());
        // 添加一些随机的粒子
        float sign = Math.random() &gt; 0.5d ? -1.0f : 1.0f;
        float speedInc = sign * (float)Math.random() * this.speedRndRange;
        float posInc = sign * (float)Math.random() * this.positionRndRange;        
        float scaleInc = sign * (float)Math.random() * this.scaleRndRange;        
        particle.getPosition().add(posInc, posInc, posInc);
        particle.getSpeed().add(speedInc, speedInc, speedInc);
        particle.setScale(particle.getScale() + scaleInc);
        particles.add(particle);
    }

    /**
     * 更新一个粒子的位置
     * @param particle 需要更新的粒子
     * @param elapsedTime 已经过的时间（毫秒）
     */
    public void updatePosition(Particle particle, long elapsedTime) {
        Vector3f speed = particle.getSpeed();
        float delta = elapsedTime / 1000.0f;
        float dx = speed.x * delta;
        float dy = speed.y * delta;
        float dz = speed.z * delta;
        Vector3f pos = particle.getPosition();
        particle.setPosition(pos.x + dx, pos.y + dy, pos.z + dz);
    }

    @Override
    public void cleanup() {
        for (GameItem particle : getParticles()) {
            particle.cleanup();
        }
    }
}
</code></pre>

<p>现在，我们可以拓展<code>Scene</code>类中包含的数据，使其包含一个<code>ParticleEmitter</code>的实例数组。</p>
<pre><code class="java">package org.lwjglb.engine;

// 这是导入……

public class Scene {

    // 这有更多属性……

    private IParticleEmitter[] particleEmitters;
</code></pre>

<p>在该阶段，我们可以开始渲染粒子。粒子不会受到光的影响，也不会产生任何音乐。它们不会有任何骨骼动画，所以用特定的着色器渲染它们是没有意义的。着色器非常简单，它们只会使用投影和模型观察矩阵渲染顶点，并使用纹理设置颜色。</p>
<p>顶点着色器的定义如下：</p>
<pre><code class="glsl">#version 330

layout (location=0) in vec3 position;
layout (location=1) in vec2 texCoord;
layout (location=2) in vec3 vertexNormal;

out vec2 outTexCoord;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

void main()
{
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    outTexCoord = texCoord;
}
</code></pre>

<p>片元着色器的定义如下：</p>
<pre><code class="glsl">#version 330

in vec2 outTexCoord;
in vec3 mvPos;
out vec4 fragColor;

uniform sampler2D texture_sampler;

void main()
{
    fragColor = texture(texture_sampler, outTexCoord);
}
</code></pre>

<p>如你所见，它们非常简单，就像渲染一章中使用的着色器。现在，和其他章节一样，我们需要在<code>Renderer</code>类中设置和使用这些着色器。着色器的设置将在一个名为<code>setupParticlesShader</code>的方法中完成，其定义如下：</p>
<pre><code class="java">private void setupParticlesShader() throws Exception {
    particlesShaderProgram = new ShaderProgram();
    particlesShaderProgram.createVertexShader(Utils.loadResource(&quot;/shaders/particles_vertex.vs&quot;));
    particlesShaderProgram.createFragmentShader(Utils.loadResource(&quot;/shaders/particles_fragment.fs&quot;));
    particlesShaderProgram.link();

    particlesShaderProgram.createUniform(&quot;projectionMatrix&quot;);
    particlesShaderProgram.createUniform(&quot;modelViewMatrix&quot;);
    particlesShaderProgram.createUniform(&quot;texture_sampler&quot;);
}
</code></pre>

<p>现在我们可以在Renderer类中创建渲染方法<code>renderParticles</code>，定义如下：</p>
<pre><code class="java">private void renderParticles(Window window, Camera camera, Scene scene) {
    particlesShaderProgram.bind();

    particlesShaderProgram.setUniform(&quot;texture_sampler&quot;, 0);
    Matrix4f projectionMatrix = transformation.getProjectionMatrix();
    particlesShaderProgram.setUniform(&quot;projectionMatrix&quot;, projectionMatrix);

    Matrix4f viewMatrix = transformation.getViewMatrix();
    IParticleEmitter[] emitters = scene.getParticleEmitters();
    int numEmitters = emitters != null ? emitters.length : 0;

    for (int i = 0; i &lt; numEmitters; i++) {
        IParticleEmitter emitter = emitters[i];
        Mesh mesh = emitter.getBaseParticle().getMesh();

        mesh.renderList((emitter.getParticles()), (GameItem gameItem) -&gt; {
            Matrix4f modelViewMatrix = transformation.buildModelViewMatrix(gameItem, viewMatrix);
            particlesShaderProgram.setUniform(&quot;modelViewMatrix&quot;, modelViewMatrix);
        }
        );
    }
    particlesShaderProgram.unbind();
}
</code></pre>

<p>如果你努力阅读，上述代码应该是不言自明的，它只是设置必要的Uniform，并渲染每个粒子。现在，我们已经创建了测试粒子效果实现所需的所有方法，只需要修改<code>DummyGame</code>类，我们就可以创建粒子发射器和基本粒子的特性。</p>
<pre><code class="java">Vector3f particleSpeed = new Vector3f(0, 1, 0);
particleSpeed.mul(2.5f);
long ttl = 4000;
int maxParticles = 200;
long creationPeriodMillis = 300;
float range = 0.2f;
float scale = 0.5f;
Mesh partMesh = OBJLoader.loadMesh(&quot;/models/particle.obj&quot;);
Texture texture = new Texture(&quot;/textures/particle_tmp.png&quot;);
Material partMaterial = new Material(texture, reflectance);
partMesh.setMaterial(partMaterial);
Particle particle = new Particle(partMesh, particleSpeed, ttl);
particle.setScale(scale);
particleEmitter = new FlowParticleEmitter(particle, maxParticles, creationPeriodMillis);
particleEmitter.setActive(true);
particleEmitter.setPositionRndRange(range);
particleEmitter.setSpeedRndRange(range);
this.scene.setParticleEmitters(new FlowParticleEmitter[] {particleEmitter});
</code></pre>

<p>我们现在使用一个普通填充圆作为粒子的纹理，以便更好地理解发生了什么。如果你运行它，你会看到如下所示的东西：</p>
<p><img alt="粒子I" src="../_static/20/particles_i.png" /></p>
<p>为什么一些粒子似乎被切断了？为什么透明的背景不能解决这个问题？原因是深度测试。粒子的一些片元被丢弃，因为它们具有比该区域的深度缓冲的当前值高的深度值。我们可以通过将其与摄像机之间的距离来排序粒子以解决这个问题，或者我们可以禁用深度写入。</p>
<p>在绘制粒子之前我们需要插入这一行代码：</p>
<pre><code class="java">glDepthMask(false);
</code></pre>

<p>然后在我们完成渲染之后还原为先前值：</p>
<pre><code class="java">glDepthMask(true);
</code></pre>

<p>然后我们会得到如下所示的东西：</p>
<p><img alt="粒子II" src="../_static/20/particles_ii.png" /></p>
<p>好了，问题解决。然而，我们仍想应用另一种效果，我们希望颜色被混合，因此颜色将被添加，以达成更好的效果。这是在渲染前增加如下一行代码来实现的：</p>
<pre><code class="java">glBlendFunc(GL_SRC_ALPHA, GL_ONE);
</code></pre>

<p>与深度的情况一样，渲染完所有粒子后，我们将混合函数恢复为：</p>
<pre><code class="java">glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
</code></pre>

<p>现在我们得到这样的效果：</p>
<p><img alt="粒子III" src="../_static/20/particles_iii.png" /></p>
<p>但我们还没有完成它。如果你把摄像机移到蓝色正方形的上方往下看，你可能会得到这样的东西：</p>
<p><img alt="粒子IV" src="../_static/20/particles_iv.png" /></p>
<p>这些粒子看起来不太好，它们应该是圆的，但现在看起来像一张纸。在此之上，我们应该应用面板技术。用于渲染粒子的四边形应该始终面向摄像机，与摄像机方向完全垂直，就好像根本没有旋转一样。摄像机的矩阵将位移和旋转应用于场景中的每一个对象，我们想跳过将要应用的旋转。</p>
<p>警告：在讲数学知识时，如果你觉得不舒服，你可以跳过它。让我们再次回顾那个观察矩阵。该矩阵可以像这样表示（没有应用任何缩放）。</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{r_{00}} & \color{red}{r_{10}} & \color{red}{r_{20}} & \color{blue}{dx} \\
\color{red}{r_{01}} & \color{red}{r_{11}} & \color{red}{r_{21}} & \color{blue}{dy} \\
\color{red}{r_{02}} & \color{red}{r_{12}} & \color{red}{r_{22}} & \color{blue}{dz} \\
0 & 0 & 0 & 1
\end{bmatrix}</script>
</script>
</p>
<p>红色的元素代表摄像机的旋转，蓝色的元素代表位移。我们需要取消观察矩阵中的左上角3x3矩阵的旋转效果，所以它会变成这样：</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{1} & \color{red}{0} & \color{red}{0} & \color{blue}{dx} \\
\color{red}{0} & \color{red}{1} & \color{red}{0} & \color{blue}{dy} \\
\color{red}{0} & \color{red}{0} & \color{red}{1} & \color{blue}{dz} \\
0 & 0 & 0 & 1
\end{bmatrix}</script>
</script>
</p>
<p>在左上角的红色部分，我们有一个3x3矩阵，把它命名为<script type="math/tex">M_{r}</script>并且我们想把它转换成单位矩阵：<script type="math/tex">I</script>。任何矩阵乘以它的逆矩阵都会得到单位矩阵：<script type="math/tex">M_{r} \times M_{r}^{-1} = I</script>。我们只需要从观察矩阵中取左上角的3x3矩阵，然后乘以它的逆矩阵，但是还可以优化他。一个旋转矩阵有一个有趣的定理，它的逆矩阵与其转置矩阵相等。即：<script type="math/tex">M_{r} \times M_{r}^{-1} = M_{r} \times M_{r}^{T} = I</script>。转置矩阵比逆矩阵更容易计算。矩阵的转置就像将其反转过来，将每一列与每一行替换。</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
r_{00} & r_{10} & r_{20} \\
r_{01} & r_{11} & r_{21} \\
r_{02} & r_{12} & r_{22}
\end{bmatrix}</script>^{T} 
=
<script type="math/tex; mode=display">\begin{bmatrix}
r_{00} & r_{01} & r_{02} \\
r_{10} & r_{11} & r_{12} \\
r_{20} & r_{21} & r_{22}
\end{bmatrix}</script>
</script>
</p>
<p>好的，让我们总结一下。我们有该变换：<script type="math/tex">V \times M</script>，其中<script type="math/tex">V</script>是观察矩阵，<script type="math/tex">M</script>是模型矩阵。我们可以这样表达：</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{v_{00}} & \color{red}{v_{10}} & \color{red}{v_{20}} & v_{30} \\
\color{red}{v_{01}} & \color{red}{v_{11}} & \color{red}{v_{21}} & v_{31} \\
\color{red}{v_{02}} & \color{red}{v_{12}} & \color{red}{v_{22}} & v_{32} \\
v_{03} & v_{13} & v_{23} & v_{33}
\end{bmatrix}</script>
\times
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{m_{00}} & \color{red}{m_{10}} & \color{red}{m_{20}} & m_{30} \\
\color{red}{m_{01}} & \color{red}{m_{11}} & \color{red}{m_{21}} & m_{31} \\
\color{red}{m_{02}} & \color{red}{m_{12}} & \color{red}{m_{22}} & m_{32} \\
m_{03} & m_{13} & m_{23} & m_{33}
\end{bmatrix}</script>
</script>
</p>
<p>我们想要取消观察矩阵的旋转，得到这样的结果：</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{1} & \color{red}{0} & \color{red}{0} & mv_{30} \\
\color{red}{0} & \color{red}{1} & \color{red}{0} & mv_{31} \\
\color{red}{0} & \color{red}{0} & \color{red}{1} & mv_{32} \\
mv_{03} & mv_{13} & mv_{23} & mv_{33}
\end{bmatrix}</script>
</script>
</p>
<p>所以我们只需要将模型矩阵的左上3x3矩阵设为观察矩阵上3x3部分的转置矩阵。</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{v_{00}} & \color{red}{v_{10}} & \color{red}{v_{20}} & v_{30} \\
\color{red}{v_{01}} & \color{red}{v_{11}} & \color{red}{v_{21}} & v_{31} \\
\color{red}{v_{02}} & \color{red}{v_{12}} & \color{red}{v_{22}} & v_{32} \\
v_{03} & v_{13} & v_{23} & v_{33}
\end{bmatrix}</script>
\times
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{v_{00}} & \color{red}{v_{01}} & \color{red}{v_{02}} & m_{30} \\
\color{red}{v_{10}} & \color{red}{v_{11}} & \color{red}{v_{12}} & m_{31} \\
\color{red}{v_{20}} & \color{red}{v_{21}} & \color{red}{v_{22}} & m_{32} \\
m_{03} & m_{13} & m_{23} & m_{33}
\end{bmatrix}</script>
</script>
</p>
<p>但在这之后，我们去掉了缩放，实际上真正想要达到的结果是这样：</p>
<p>
<script type="math/tex">
<script type="math/tex; mode=display">\begin{bmatrix}
\color{red}{sx} & \color{red}{0} & \color{red}{0} & mv_{30} \\
\color{red}{0} & \color{red}{sy} & \color{red}{0} & mv_{31} \\
\color{red}{0} & \color{red}{0} & \color{red}{sz} & mv_{32} \\
mv_{03} & mv_{13} & mv_{23} & mv_{33}
\end{bmatrix}</script>
</script>
</p>
<p>其中sx，sy和sz就是缩放系数。因此，当我们将模型矩阵的左上3x3矩阵设置为观察矩阵的转置矩阵后，我们需要再次应用缩放。</p>
<p>就这些，我们只需要在<code>renderParticlesMethod</code>中像这样修改：</p>
<pre><code class="java">        for (int i = 0; i &lt; numEmitters; i++) {
            IParticleEmitter emitter = emitters[i];
            Mesh mesh = emitter.getBaseParticle().getMesh();

            mesh.renderList((emitter.getParticles()), (GameItem gameItem) -&gt; {
                Matrix4f modelMatrix = transformation.buildModelMatrix(gameItem);

                viewMatrix.transpose3x3(modelMatrix);

                Matrix4f modelViewMatrix = transformation.buildModelViewMatrix(modelMatrix, viewMatrix);
                modelViewMatrix.scale(gameItem.getScale());
                particlesShaderProgram.setUniform(&quot;modelViewMatrix&quot;, modelViewMatrix);
            }
            );
        }
</code></pre>

<p>我们还在<code>Transformation</code>类中添加了另一种方法，使用两个矩阵来构造模型观察矩阵，而不是使用<code>GameItem</code>和观察矩阵。</p>
<p>有了如上更改，当从上方观察粒子时，我们就得到如下结果：</p>
<p><img alt="粒子V" src="../_static/20/particles_v.png" /></p>
<p>现在集齐了创建一个更真实的粒子效果所需要的所有要素，所以让我们将其改为更精细的纹理。我们将使用如下图片（它是由<a href="https://www.gimp.org/">GIMP</a>创作的，带有光照和阴影过滤器）：</p>
<p><img alt="粒子纹理" src="../_static/20/particle_texture.png" /></p>
<p>有了如上纹理，我们会得到如下所示的粒子：</p>
<p><img alt="粒子VI" src="../_static/20/particles_vi.png" /></p>
<p>现在更好了！你可能会注意到我们需要调整缩放，因为粒子现在总是对着摄像机，显示的面积总是最大的。</p>
<p>最后，再提一点，为了得到可以在任何场景使用的完美的效果，你需要实现粒子排序和启用深度缓冲区。无论如何，这里有一个示例可以将这种效果囊括到你的游戏中。</p>
<h2 id="texture-atlas">纹理集（Texture Atlas）</h2>
<p>现在我们已经做好了粒子效果的基础建设，现在可以为它添加一些动画效果了。为了实现它，我们将支持纹理集。纹理集（Texture Atlas）是一个包含所有将要使用的纹理的大型图片。使用纹理集，我们就只需要加载一个大的图片，然后再绘制游戏项时，选择该图像的一部分作为纹理。例如，当我们想用不同的纹理多次渲染相同的模型时（例如树或岩石），可以使用这种技术。我们可以使用相同的纹理集并选择适当的坐标，而不是使用很多纹理实例并在它们之间切换（记住，切换状态总是很慢的）。</p>
<p>在此情况下，我们将使用纹理坐标来添加粒子动画。我们遍历不同的纹理来为粒子动画建模，所有这些纹理将被分到一个像这样的纹理集：</p>
<p><img alt="纹理集" src="../_static/20/texture_atlas.png" /></p>
<p>纹理集可以被划分为多个方形片段。我们将一个方形片段坐标分配到一个粒子上，并随着时间推移改变它以表示动画。让我们开始吧。我们要做的第一件事是修改<code>Texture</code>类来指定纹理集可以拥有的行数和列数。</p>
<pre><code class="java">package org.lwjglb.engine.graph;

// .. 这里是导入

public class Texture {

    // 无关属性省略
    private int numRows = 1;

    private int numCols = 1;

   // 无关代码省略

    public Texture(String fileName, int numCols, int numRows) throws Exception  {
        this(fileName);
        this.numCols = numCols;
        this.numRows = numRows;
    }
</code></pre>

<p>默认情况下，我们处理的纹理的列数和行数等于1。我们还添加了另一个构造函数来指定行和列。</p>
<p>然后，我们需要追踪一个<code>GameItem</code>在纹理集中的坐标，因此只需向该类添加另一个属性，默认值为0。</p>
<pre><code class="java">package org.lwjglb.engine.items;

import org.joml.Vector3f;
import org.lwjglb.engine.graph.Mesh;

public class GameItem {

    // 更多属性省略

    private int textPos;
</code></pre>

<p>然后我们修改<code>Particle</code>类，以便能够通过纹理集自动迭代。</p>
<pre><code class="java">package org.lwjglb.engine.graph.particles;

import org.joml.Vector3f;
import org.lwjglb.engine.graph.Mesh;
import org.lwjglb.engine.graph.Texture;
import org.lwjglb.engine.items.GameItem;

public class Particle extends GameItem {

    private long updateTextureMillis;

    private long currentAnimTimeMillis;
</code></pre>

<p>属性<code>updateTextureMillis</code>定义移动到纹理集中下一个坐标的时间（以毫秒为单位）。数值月底，粒子在纹理上变化的速度就越快。属性<code>currentAnimTimeMillis</code>只是跟踪纹理持续当前纹理坐标的时间。</p>
<p>因此，我们需要修改<code>Particle</code>类构造函数来设置这些值。我们还计算了纹理集的片段数量，它是由属性<code>animFrames</code>定义的。</p>
<pre><code class="java">public Particle(Mesh mesh, Vector3f speed, long ttl, long updateTextureMillis) {
    super(mesh);
    this.speed = new Vector3f(speed);
    this.ttl = ttl;
    this.updateTextureMills = updateTextureMills;
    this.currentAnimTimeMillis = 0;
    Texture texture = this.getMesh().getMaterial().getTexture();
    this.animFrames = texture.getNumCols() * texture.getNumRows();
}
</code></pre>

<p>现在，我们只需要修改检查粒子是否已经过期的方法，来检查是否需要更新纹理坐标。</p>
<pre><code class="java">public long updateTtl(long elapsedTime) {
    this.ttl -= elapsedTime;
    this.currentAnimTimeMillis += elapsedTime;
    if ( this.currentAnimTimeMillis &gt;= this.getUpdateTextureMillis() &amp;&amp; this.animFrames &gt; 0 ) {
        this.currentAnimTimeMillis = 0;
        int pos = this.getTextPos();
        pos++;
        if ( pos &lt; this.animFrames ) {
            this.setTextPos(pos);
        } else {
            this.setTextPos(0);
        }
    }
    return this.ttl;
}
</code></pre>

<p>除此之外，我们还修改了<code>FlowRangeEmitter</code>类，在应该改变粒子纹理坐标的时间周期上增加了一些随机性。你可以在源代码上查看它。</p>
<p>现在，我们可以使用这些数据来设置合适的纹理坐标。我们将在顶点着色器中进行这一操作，因为它输出了要在片元着色器中使用的那些值。这个新着色器的定义如下：</p>
<pre><code class="glsl">#version 330

layout (location=0) in vec3 position;
layout (location=1) in vec2 texCoord;
layout (location=2) in vec3 vertexNormal;

out vec2 outTexCoord;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

uniform float texXOffset;
uniform float texYOffset;
uniform int numCols;
uniform int numRows;

void main()
{
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

    // Support for texture atlas, update texture coordinates
    float x = (texCoord.x / numCols + texXOffset);
    float y = (texCoord.y / numRows + texYOffset);

    outTexCoord = vec2(x, y);
}
</code></pre>

<p>如你所见，我们现在有三个新Uniform。Uniform<code>numCols</code>和<code>numRows</code>只储存纹理集的列和行数。为了计算纹理坐标，首先必须缩小这些参数。每个片段的宽度为<script type="math/tex">1 / numCols</script>，高度为<script type="math/tex">1 / numRows</script>，如下图所示。</p>
<p><img alt="纹理坐标" src="../_static/20/texture_coordinates.png" /></p>
<p>然后我们只需要根据行和列应用和偏移，这是由<code>texXOffset</code>和<code>texYOffset</code>Uniform定义的。</p>
<p>我们将在<code>Renderer</code>类中计算这些偏移量，如下述代码所示。我们根据每个粒子的坐标计算它们所处的行和列，并将偏移量计算为片段高度和宽度的倍数。</p>
<pre><code class="java">mesh.renderList((emitter.getParticles()), (GameItem gameItem) -&gt; {
    int col = gameItem.getTextPos() % text.getNumCols();
    int row = gameItem.getTextPos() / text.getNumCols();
    float textXOffset = (float) col / text.getNumCols();
    float textYOffset = (float) row / text.getNumRows();
    particlesShaderProgram.setUniform(&quot;texXOffset&quot;, textXOffset);
    particlesShaderProgram.setUniform(&quot;texYOffset&quot;, textYOffset);
</code></pre>

<p>注意，如果你只需要支持正方形纹理集，你只需要两个Uniform。最终的效果是这样的：</p>
<p><img alt="粒子动画" src="../_static/20/animated_particles.png" /></p>
<p>现在，我们有了粒子动画。在下章中，我们讲学习如何优化渲染流程。我们正在渲染具有相同网格的多个元素，并为每个元素进行绘制调用。在下章中，我们讲学习如何在单个调用中渲染它们。这种技术不仅适用于粒子，也适用于渲染共享同一模型，但被放在不同位置或具有不同纹理的多个元素的场景。</p>
              
            </div>
          </div>
          <footer>
  
    <div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
      
        <a href="../21-instanced-rendering/" class="btn btn-neutral float-right" title="实例化渲染">Next <span class="icon icon-circle-arrow-right"></span></a>
      
      
        <a href="../19-animations/" class="btn btn-neutral" title="动画"><span class="icon icon-circle-arrow-left"></span> Previous</a>
      
    </div>
  

  <hr/>

  <div role="contentinfo">
    <!-- Copyright etc -->
    
      <p>2019, Mouse0w0</p>
    
  </div>

  Built with <a href="https://www.mkdocs.org/">MkDocs</a> using a <a href="https://github.com/snide/sphinx_rtd_theme">theme</a> provided by <a href="https://readthedocs.org">Read the Docs</a>.
</footer>
      
        </div>
      </div>

    </section>

  </div>

  <div class="rst-versions" role="note" aria-label="versions">
    <span class="rst-current-version" data-toggle="rst-current-version">
      
          <a href="https://github.com/Mouse0w0/lwjglbook-CN-Translation/" class="fa fa-github" style="float: left; color: #fcfcfc"> GitHub</a>
      
      
        <span><a href="../19-animations/" style="color: #fcfcfc;">&laquo; Previous</a></span>
      
      
        <span style="margin-left: 15px"><a href="../21-instanced-rendering/" style="color: #fcfcfc">Next &raquo;</a></span>
      
    </span>
</div>
    <script>var base_url = '..';</script>
    <script src="../js/theme.js" defer></script>
      <script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" defer></script>
      <script src="../search/main.js" defer></script>
    <script defer>
        window.onload = function () {
            SphinxRtdTheme.Navigation.enable(true);
        };
    </script>

</body>
</html>
